/*
 * File     : BaseNativeCallFactory.java
 * Created  : 27 Apr 2011
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni.internal.call;

import static com.googlecode.dni.internal.DniInternal.LOGGER;
import static com.googlecode.dni.internal.DniTypes.SCRATCH_MEMORY_TYPE;
import static org.objectweb.asm.Opcodes.*;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.nio.Buffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.logging.Level;

import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;

import com.googlecode.dni.ByValue;
import com.googlecode.dni.FixedSize;
import com.googlecode.dni.IllegalLibraryException;
import com.googlecode.dni.internal.DniInternal;
import com.googlecode.dni.internal.library.ScratchMemory;
import com.googlecode.dni.internal.string.FastCharset;
import com.googlecode.dni.type.NativeObject;
import com.googlecode.dni.type.Pointer;
import com.googlecode.dni.type.nativetype.NativeTypes;
import com.googlecode.dni.type.structure.Structure;


/**
 * Base implementation of {@link NativeCallFactory}.
 *
 * @author Matthew Wilson
 */
// TODO determine whether this is necessary, or can be merged with NCF
abstract class BaseNativeCallFactory extends NativeCallFactory
{

    private final Map< String, Parameter > createdFields = new HashMap< String, Parameter >();


    /**
     * {@inheritDoc}
     */
    @Override
    public JniNativeMethod writeMethod( final Method method,
                                        final Pointer functionPointer,
                                        final String charset )
    {
        LOGGER.fine( "Creating library method: " + method );

        // 0) work out what the native call's return type will be
        Parameter returnParameter = getReturnParameter( method, charset );

        // 1) determine what native parameters are required
        List< Parameter > parameters = new ArrayList< Parameter >();
        determineParameters( method, parameters, charset );

        // 2) determine what native binding is required
        List< Parameter > returnParameterContainer = Arrays.asList( returnParameter );
        Pointer nativeBinding = getNativeBinding( returnParameterContainer, parameters, functionPointer );
        returnParameter = returnParameterContainer.get( 0 );

        if ( LOGGER.isLoggable( Level.FINE ) )
        {
            for ( Parameter parameter : parameters )
            {
                LOGGER.fine( " * " + parameter );
            }
        }

        // 3) create the native method
        JniNativeMethod nativeMethod = createNativeMethod( returnParameter.getNativeType(),
                                                           parameters,
                                                           nativeBinding );

        LOGGER.fine( "Creating native method " + nativeMethod + " for " + method );

        // 4) create the Java method
        createJavaMethod( method, returnParameter, parameters, nativeMethod );

        return nativeMethod;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeStaticInitialiser()
    {
        if ( this.createdFields.isEmpty() )
        {
            return;
        }

        MethodVisitor methodVisitor = getClassWriter().visitMethod( ACC_STATIC | ACC_PRIVATE,
                                                                    "<clinit>",
                                                                    "()V",
                                                                    null,
                                                                    null );

        // a parameter *might* have more than one field: only call each one once
        for ( Parameter parameter : new LinkedHashSet< Parameter >( this.createdFields.values() ) )
        {
            parameter.writeStaticInitialiserFragment( methodVisitor );
        }

        methodVisitor.visitInsn( RETURN );

        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    /**
     * <p>
     *  Determines what the return type will be.
     * </p>
     *
     * @param method
     *            the method
     * @param charset
     *            the charset for the function
     *
     * @return the return type
     */
    private Parameter getReturnParameter( final Method method,
                                          final String charset )
    {
        Parameter returnParameter = null;
        Class< ? > returnType = method.getReturnType();

        boolean isByValue = method.getAnnotation( ByValue.class ) != null;

        if ( returnType == void.class )
        {
            returnParameter = new VoidParameter();
        }
        else if ( returnType.isPrimitive() )
        {
            // we can support all primitive types
            returnParameter = new PrimitiveParameter( method );
        }
        else if ( returnType == String.class )
        {
            returnParameter = new StringParameter( FastCharset.getInstance( charset ) );
        }
        else if ( returnType == Pointer.class )
        {
            returnParameter = new PointerParameter();
        }
        else if ( Enum.class.isAssignableFrom( returnType ) )
        {
            returnParameter = new EnumParameter();
        }
        else if ( Buffer.class.isAssignableFrom( returnType ) )
        {
            FixedSize fixedSize = method.getAnnotation( FixedSize.class );
            if ( fixedSize == null )
            {
                throw new IllegalLibraryException( "Returned ByteBuffer must be @FixedSize: " + method );
            }

            returnParameter = new BufferParameter( fixedSize.value() );
        }
        else if ( Structure.class.isAssignableFrom( returnType ) )
        {
            if ( isByValue )
            {
                returnParameter = new StructureByValueParameter();
            }
            else
            {
                returnParameter = new StructureByReferenceParameter();
            }
        }

        if ( returnParameter == null )
        {
            throw new IllegalLibraryException( "Unsupported return type: " + method );
        }

        returnParameter.setJavaType( returnType );
        return returnParameter;
    }

    /**
     * <p>
     *  Determines what native parameters are needed.
     * </p>
     *
     * @param method
     *            the method
     * @param parameters
     *            the destination parameters list
     * @param charset
     *            the charset for the function
     */
    private void determineParameters( final Method method,
                                      final List< Parameter > parameters,
                                      final String charset )
    {
        Class< ? >[] parameterTypes = method.getParameterTypes();
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();

        int javaVarIndex = 1;
        for ( int i = 0; i < parameterTypes.length; i++ )
        {
            Class< ? > parameterType = parameterTypes[ i ];
            boolean isByValue = annotationPresent( parameterAnnotations[ i ], ByValue.class );

            Parameter parameter;

            if ( parameterType.isPrimitive() )
            {
                // we can support all primitive types
                parameter = new PrimitiveParameter( method, parameterType, parameterAnnotations[ i ] );
            }
            else if ( parameterType == String.class )
            {
                parameter = new StringParameter( FastCharset.getInstance( charset ) );
            }
            else if ( parameterType == Pointer.class )
            {
                parameter = new PointerParameter();
            }
            else if ( isByValue && Structure.class.isAssignableFrom( parameterType ) )
            {
                parameter = new StructureByValueParameter();
            }
            else if ( NativeObject.class.isAssignableFrom( parameterType ) )
            {
                if ( isByValue )
                {
                    throw new IllegalLibraryException( "Cannot have arbitrary NativeObject by value: " + method );
                }

                parameter = new NativeObjectParameter();
            }
            else if ( Enum.class.isAssignableFrom( parameterType ) )
            {
                parameter = new EnumParameter();
            }
            else if ( Buffer.class.isAssignableFrom( parameterType ) )
            {
                FixedSize fixedSize = method.getAnnotation( FixedSize.class );
                parameter = new BufferParameter( fixedSize != null ? fixedSize.value() : -1 );
            }
            else if ( i == parameterTypes.length - 1 && parameterType.isArray() && method.isVarArgs() )
            {
                parameter = new VariableArityParameter();
            }
            else
            {
                throw new IllegalLibraryException( "Unsupported parameter type: " + method );
            }

            javaVarIndex = addParameter( parameters, parameter, parameterType, i, javaVarIndex );
        }
    }

    private int addParameter( final List< Parameter > parameters,
                              final Parameter parameter,
                              final Class< ? > javaType,
                              final int javaIndex,
                              final int javaVarIndex )
    {
        parameter.setJavaType( javaType );
        parameter.setJavaIndex( javaIndex );
        parameter.setJavaVarIndex( javaVarIndex );
        parameter.setLibraryImplName( getImplName() );
        parameters.add( parameter );
        return javaVarIndex + parameter.getJavaVarSlots();
    }

    /**
     * <p>
     *  Determines which native binding to use.  Must also insert a function
     *  pointer parameter into the list of parameters if one is required.
     * </p>
     *
     * @param returnParameterContainer
     *            the return parameter in a singleton container; may be modified
     * @param parameters
     *            the parameters; may be modified
     * @param functionPointer
     *            the function pointer to the native function
     *
     * @return a pointer to the native binding
     */
    abstract Pointer getNativeBinding( List< Parameter > returnParameterContainer,
                                       List< Parameter > parameters,
                                       Pointer functionPointer );

    /**
     * <p>
     *  Creates the native method.
     * </p>
     *
     * @param nativeReturnSignature
     *            the return type of the native method
     * @param parameters
     *            the parameters
     * @param nativeBinding
     *            the native binding
     *
     * @return the native method
     */
    private JniNativeMethod createNativeMethod( final char nativeReturnSignature,
                                                final List< Parameter > parameters,
                                                final Pointer nativeBinding )
    {
        StringBuilder signature = new StringBuilder( parameters.size() + 8 );

        signature.append( '(' );

        for ( Parameter parameter : parameters )
        {
            signature.append( parameter.getNativeType() );
        }
        signature.append( ')' ).append( nativeReturnSignature );

        String name = generateNativeMethodName();
        MethodVisitor methodVisitor = getClassWriter().visitMethod( ACC_PRIVATE | ACC_NATIVE | ACC_STATIC,
                                                                    name,
                                                                    signature.toString(),
                                                                    null,
                                                                    null );
        methodVisitor.visitEnd();

        JniNativeMethod nativeMethod = new JniNativeMethod( name,
                                                            signature.toString(),
                                                            nativeBinding );

        return nativeMethod;
    }

    /**
     * <p>
     *  Create the Java method that does the unwrapping and wrapping.
     * </p>
     *
     * @param method
     *            the method
     * @param returnParameter
     *            the return parameter
     * @param parameters
     *            the parameters, in native order
     * @param nativeMethod
     *            the native method
     */
    private void createJavaMethod( final Method method,
                                   final Parameter returnParameter,
                                   final List< Parameter > parameters,
                                   final JniNativeMethod nativeMethod )
    {
        MethodVisitor methodVisitor = getClassWriter().visitMethod( ACC_PUBLIC,
                                                                    method.getName(),
                                                                    Type.getMethodDescriptor( method ),
                                                                    null,
                                                                    null );

        Label methodStart = new Label();
        methodVisitor.visitLabel( methodStart );

        int varCount = 1;
        boolean needsTidying = false;
        boolean needsScratch = false;

        // variables for holding scratch data
        int scratchTotalSizeVarIndex = -1;
        int scratchMemoryVarIndex = -1;
        int scratchPointerVarIndex = -1;
        int scratchBufferVarIndex = -1;

        for ( Parameter parameter : parameters )
        {
            // count the number of slots used by Java parameters
            varCount += parameter.getJavaVarSlots();

            // create any fields
            parameter.allocateFields( getClassWriter(), this.createdFields );

            // determine whether tidying is required
            if ( parameter.needsTidying() )
            {
                needsTidying = true;
            }

            // determine whether scratch is required
            if ( parameter.needsScratchMemory() )
            {
                needsScratch = true;
                needsTidying = true;
            }
        }

        // count the number of slots used by prepared variables
        for ( Parameter parameter : parameters )
        {
            varCount += parameter.allocatePrepareVarSlots( methodVisitor, varCount );
        }

        if ( needsScratch )
        {
            scratchTotalSizeVarIndex = varCount++;
            scratchMemoryVarIndex = varCount++;
            scratchPointerVarIndex = varCount;
            varCount += 2;
            scratchBufferVarIndex = varCount++;

            varCount = writeReserveScratch( methodVisitor,
                                            parameters,
                                            varCount,
                                            scratchTotalSizeVarIndex,
                                            scratchMemoryVarIndex,
                                            scratchPointerVarIndex,
                                            scratchBufferVarIndex );
        }

        // start the try...finally... bit
        Label tryStart = new Label();
        Label tryEnd = new Label();
        Label tryHandler = new Label();

        if ( needsTidying )
        {
            methodVisitor.visitTryCatchBlock( tryStart, tryEnd, tryHandler, null );
        }

        // write any preamble
        methodVisitor.visitLabel( tryStart );
        for ( Parameter parameter : parameters )
        {
            parameter.writePrepareParameter( methodVisitor );
        }

        // fetch parameter values
        for ( Parameter parameter : parameters )
        {
            parameter.writeGetParameter( methodVisitor );
        }

        // call the native method
        methodVisitor.visitMethodInsn( INVOKESTATIC,
                                       getImplName(),
                                       nativeMethod.getName(),
                                       nativeMethod.getSignature() );

        // store the result (if any)
        int returnVarIndex = varCount;
        varCount += writeStoreReturnResult( methodVisitor, returnParameter, returnVarIndex );

        methodVisitor.visitLabel( tryEnd );

        if ( needsTidying )
        {
            writeFinallyBlock( methodVisitor,
                               parameters,
                               scratchTotalSizeVarIndex,
                               scratchMemoryVarIndex );
        }

        // retrieve the result and process
        returnParameter.writeReturnParameter( methodVisitor, returnVarIndex );

        if ( needsTidying )
        {
            writeTryHandlerBlock( methodVisitor,
                                  parameters,
                                  tryHandler,
                                  varCount,
                                  scratchTotalSizeVarIndex,
                                  scratchMemoryVarIndex );
        }

        for ( Parameter parameter : parameters )
        {
            parameter.finishedMethod( methodVisitor, methodStart );
        }

        if ( DniInternal.DEBUG_MODE )
        {
            setLocalVariables( methodVisitor,
                               returnParameter,
                               parameters,
                               methodStart,
                               returnVarIndex,
                               scratchTotalSizeVarIndex,
                               scratchMemoryVarIndex,
                               scratchPointerVarIndex,
                               scratchBufferVarIndex );
        }

        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    /**
     * Writes code to make a call to {@link ScratchMemory#reserve(int)}.
     *
     * @param methodVisitor
     *            the method visitor
     * @param parameters
     *            the parameters
     * @param startVarCount
     *            the number of variables so far
     * @param scratchTotalSizeVarIndex
     *            the slot index of the scratchTotalSize variable
     * @param scratchMemoryVarIndex
     *            the slot index of the scratchMemory variable
     * @param scratchPointerVarIndex
     *            the slot index of the scratchPointer variable
     * @param scratchBufferVarIndex
     *            the slot index of the scratchBuffer variable
     *
     * @return the resultant variable count
     */
    private int writeReserveScratch( final MethodVisitor methodVisitor,
                                     final List< Parameter > parameters,
                                     final int startVarCount,
                                     final int scratchTotalSizeVarIndex,
                                     final int scratchMemoryVarIndex,
                                     final int scratchPointerVarIndex,
                                     final int scratchBufferVarIndex )
    {
        int varCount = startVarCount;

        // write code to calculate scratch memory required
        List< Integer > previousScratchMemorySizeVariables = new ArrayList< Integer >();
        for ( Parameter parameter : parameters )
        {
            if ( parameter.needsScratchMemory() )
            {
                parameter.writeScratchMemorySize( methodVisitor );
                methodVisitor.visitVarInsn( ISTORE, varCount );

                Integer[] previousVars = previousScratchMemorySizeVariables.toArray( new Integer[ 0 ] );

                parameter.setScratchVariables( varCount,
                                               new ScratchPointerWriter( methodVisitor, scratchPointerVarIndex, previousVars ),
                                               new ScratchBufferWriter( methodVisitor, scratchBufferVarIndex, previousVars ) );

                previousScratchMemorySizeVariables.add( varCount );

               varCount++;
            }
        }

        // add it all up
        boolean add = false;
        for ( Parameter parameter : parameters )
        {
            if ( parameter.needsScratchMemory() )
            {
                methodVisitor.visitVarInsn( ILOAD, parameter.getScratchMemorySizeVarIndex() );

                if ( add )
                {
                    methodVisitor.visitInsn( IADD );
                }
                add = true;
            }
        }

        assert add : "Must have at least one size variable";

        // round it up to the nearest 32 bytes
        methodVisitor.visitIntInsn( BIPUSH, 31 );
        methodVisitor.visitInsn( IADD );
        methodVisitor.visitLdcInsn( ~31 );
        methodVisitor.visitInsn( IAND );

        methodVisitor.visitVarInsn( ISTORE, scratchTotalSizeVarIndex );

        // write code to reserve the space
        methodVisitor.visitVarInsn( ILOAD, scratchTotalSizeVarIndex );
        methodVisitor.visitMethodInsn( INVOKESTATIC,
                                       SCRATCH_MEMORY_TYPE,
                                       "reserve",
                                       "(I)L" + SCRATCH_MEMORY_TYPE + ";" );
        methodVisitor.visitVarInsn( ASTORE, scratchMemoryVarIndex );

        // get the base pointer of the scratch memory
        methodVisitor.visitVarInsn( ALOAD, scratchMemoryVarIndex );
        if ( NativeTypes.POINTER_SIZE == 4 )
        {
            methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                           SCRATCH_MEMORY_TYPE,
                                           "getPointer32",
                                           "()I" );
            methodVisitor.visitVarInsn( ISTORE, scratchPointerVarIndex );
        }
        else
        {
            methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                           SCRATCH_MEMORY_TYPE,
                                           "getPointer64",
                                           "()J" );
            methodVisitor.visitVarInsn( LSTORE, scratchPointerVarIndex );
        }

        // get the buffer of the scratch memory
        methodVisitor.visitVarInsn( ALOAD, scratchMemoryVarIndex );
        methodVisitor.visitVarInsn( ILOAD, scratchTotalSizeVarIndex );
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       SCRATCH_MEMORY_TYPE,
                                       "getBuffer",
                                       "(I)Ljava/nio/ByteBuffer;" );
        methodVisitor.visitVarInsn( ASTORE, scratchBufferVarIndex );

        return varCount;
    }

    /**
     * Writes code to get the offset for a scratch parameter, using the previous
     * variables' sizes.
     *
     * @param methodVisitor
     *            the method visitor
     * @param previousVars
     *            the previous variables
     */
    private static void writeGetOffset( final MethodVisitor methodVisitor, final Integer[] previousVars )
    {
        boolean add = false;
        for ( Integer previousVar : previousVars )
        {
            methodVisitor.visitVarInsn( ILOAD, previousVar );

            if ( add )
            {
                methodVisitor.visitInsn( IADD );
            }
            add = true;
        }
    }

    /**
     * Writes the handler block for tidying up.
     *
     * @param methodVisitor
     *            the method visitor
     * @param parameters
     *            the parameters
     * @param tryHandler
     *            the label for the try handler block
     * @param varCount
     *            the number of variables so far
     * @param scratchTotalSizeVarIndex
     *            the slot index of the scratchTotalSize variable (or -1 when no
     *            scratch was allocated)
     * @param scratchMemoryVarIndex
     *            the slot index of the scratchMemory variable
     */
    private void writeTryHandlerBlock( final MethodVisitor methodVisitor,
                                       final List< Parameter > parameters,
                                       final Label tryHandler,
                                       final int varCount,
                                       final int scratchTotalSizeVarIndex,
                                       final int scratchMemoryVarIndex )
    {
        // write any tidy up for the exception case
        methodVisitor.visitLabel( tryHandler );

        methodVisitor.visitVarInsn( ASTORE, varCount );

        writeFinallyBlock( methodVisitor,
                           parameters,
                           scratchTotalSizeVarIndex,
                           scratchMemoryVarIndex );

        methodVisitor.visitVarInsn( ALOAD, varCount );
        methodVisitor.visitInsn( ATHROW );

        if ( DniInternal.DEBUG_MODE )
        {
            Label tryHandlerEnd = new Label();
            methodVisitor.visitLabel( tryHandlerEnd );

            methodVisitor.visitLocalVariable( "throwable",
                                              "Ljava/lang/Throwable;",
                                              null,
                                              tryHandler,
                                              tryHandlerEnd,
                                              varCount );
        }
    }

    /**
     * Writes the finally block for tidying up.  This has to be written twice.
     * (JSR is not permitted in byte-code anymore.)
     *
     * @param methodVisitor
     *            the method visitor
     * @param parameters
     *            the parameters
     * @param scratchTotalSizeVarIndex
     *            the slot index of the scratchTotalSize variable (or -1 when no
     *            scratch was allocated)
     * @param scratchMemoryVarIndex
     *            the slot index of the scratchMemory variable
     */
    private void writeFinallyBlock( final MethodVisitor methodVisitor,
                                    final List< Parameter > parameters,
                                    final int scratchTotalSizeVarIndex,
                                    final int scratchMemoryVarIndex )
    {
        for ( ListIterator< Parameter > it = parameters.listIterator( parameters.size() );
              it.hasPrevious(); )
        {
            it.previous().writeTidyParameter( methodVisitor );
        }

        if ( scratchTotalSizeVarIndex > 0 )
        {
            methodVisitor.visitVarInsn( ALOAD, scratchMemoryVarIndex );
            methodVisitor.visitVarInsn( ILOAD, scratchTotalSizeVarIndex );
            methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                           SCRATCH_MEMORY_TYPE,
                                           "release",
                                           "(I)V" );
        }
    }

    /**
     * <p>
     *  Writes out byte-code to store the primitive result from the native call
     *  in the given var slot.
     * </p>
     *
     * @param methodVisitor
     *            the method visitor
     * @param returnParameter
     *            the return parameter
     * @param returnVarIndex
     *            the var slot index to use
     *
     * @return number of extra slots used
     */
    private int writeStoreReturnResult( final MethodVisitor methodVisitor,
                                        final Parameter returnParameter,
                                        final int returnVarIndex )
    {
        switch ( returnParameter.getNativeType() )
        {
            case 'V':
                return 0;

            case 'Z':
            case 'B':
            case 'S':
            case 'C':
            case 'I':
                methodVisitor.visitVarInsn( ISTORE, returnVarIndex );
                return 1;

            case 'J':
                methodVisitor.visitVarInsn( LSTORE, returnVarIndex );
                return 2;

            case 'F':
                methodVisitor.visitVarInsn( FSTORE, returnVarIndex );
                return 1;

            case 'D':
                methodVisitor.visitVarInsn( DSTORE, returnVarIndex );
                return 2;

            default:
                throw new AssertionError( returnParameter.getNativeType() );
        }

    }

    /**
     * Sets the local variables in use.
     *
     * @param methodVisitor
     *            the method visitor
     * @param returnParameter
     *            the return parameter
     * @param parameters
     *            the parameters
     * @param methodStart
     *            label for the start of the method
     * @param returnVarIndex
     *            the return var slot index
     * @param scratchTotalSizeVarIndex
     *            the slot index of the scratchTotalSize variable
     * @param scratchMemoryVarIndex
     *            the slot index of the scratchMemory variable
     * @param scratchPointerVarIndex
     *            the slot index of the scratchPointer variable
     * @param scratchBufferVarIndex
     *            the slot index of the scratchBuffer variable
     */
    private static void setLocalVariables( final MethodVisitor methodVisitor,
                                           final Parameter returnParameter,
                                           final List< Parameter > parameters,
                                           final Label methodStart,
                                           final int returnVarIndex,
                                           final int scratchTotalSizeVarIndex,
                                           final int scratchMemoryVarIndex,
                                           final int scratchPointerVarIndex,
                                           final int scratchBufferVarIndex )
    {
        Label methodEnd = new Label();
        methodVisitor.visitLabel( methodEnd );

        if ( returnParameter.getNativeType() != 'V' )
        {
            methodVisitor.visitLocalVariable( "result",
                                              Character.toString( returnParameter.getNativeType() ),
                                              null,
                                              methodStart,
                                              methodEnd,
                                              returnVarIndex );
        }

        // a Java parameter may produce more than one native parameter;
        // only set each one once
        BitSet labelledParameters = new BitSet( parameters.size() );
        for ( Parameter parameter : parameters )
        {
            int javaVarIndex = parameter.getJavaVarIndex();
            if ( javaVarIndex >= 0 && !labelledParameters.get( javaVarIndex ) )
            {
                labelledParameters.set( javaVarIndex );

                methodVisitor.visitLocalVariable( "arg" + parameter.getJavaIndex(),
                                                  Type.getDescriptor( parameter.getJavaType() ),
                                                  null,
                                                  methodStart,
                                                  methodEnd,
                                                  javaVarIndex );
            }

            if ( parameter.getScratchMemorySizeVarIndex() > 0 )
            {
                methodVisitor.visitLocalVariable( "scratchMemorySizeArg" + parameter.getJavaIndex(),
                                                  "I",
                                                  null,
                                                  methodStart,
                                                  methodEnd,
                                                  parameter.getScratchMemorySizeVarIndex() );
            }
        }

        if ( scratchTotalSizeVarIndex != -1 )
        {
            methodVisitor.visitLocalVariable( "scratchMemoryTotalSize",
                                              "I",
                                              null,
                                              methodStart,
                                              methodEnd,
                                              scratchTotalSizeVarIndex );
            methodVisitor.visitLocalVariable( "scratchMemory",
                                              "L" + SCRATCH_MEMORY_TYPE + ";",
                                              null,
                                              methodStart,
                                              methodEnd,
                                              scratchMemoryVarIndex );
            methodVisitor.visitLocalVariable( "scratchPointer",
                                              NativeTypes.POINTER_SIZE == 4 ? "I" : "J",
                                              null,
                                              methodStart,
                                              methodEnd,
                                              scratchPointerVarIndex );
            methodVisitor.visitLocalVariable( "scratchBuffer",
                                              "Ljava/nio/ByteBuffer;",
                                              null,
                                              methodStart,
                                              methodEnd,
                                              scratchBufferVarIndex );
        }
    }

    /**
     * Tests for the presence of an annotation.
     *
     * @param annotations
     *            the array of annotations
     * @param annotationType
     *            the annotation type
     * @return <code>true</code> iff present
     */
    private static boolean annotationPresent( final Annotation[] annotations, final Class< ? extends Annotation > annotationType )
    {
        for ( Annotation annotation : annotations )
        {
            if ( annotation.annotationType() == annotationType )
            {
                return true;
            }
        }

        return false;
    }

    /**
     * Writes out the scratch buffer.
     *
     * @author Matthew Wilson
     */
    private final class ScratchBufferWriter implements Runnable
    {
        private final MethodVisitor methodVisitor;

        private final int scratchBufferVarIndex;

        private final Integer[] previousVars;

        private ScratchBufferWriter( final MethodVisitor methodVisitor,
                                     final int scratchBufferVarIndex,
                                     final Integer[] previousVars )
        {
            this.methodVisitor = methodVisitor;
            this.previousVars = previousVars;
            this.scratchBufferVarIndex = scratchBufferVarIndex;
        }

        @Override
        public void run()
        {
            this.methodVisitor.visitVarInsn( ALOAD, this.scratchBufferVarIndex );

            if ( this.previousVars.length > 0 )
            {
                writeGetOffset( this.methodVisitor, this.previousVars );

                this.methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                                    "java/nio/ByteBuffer",
                                                    "position",
                                                   "(I)Ljava/nio/Buffer;" );
            }

            this.methodVisitor.visitInsn( POP );
            this.methodVisitor.visitVarInsn( ALOAD, this.scratchBufferVarIndex );
        }
    }

    /**
     * Writes out the scratch pointer.
     *
     * @author Matthew Wilson
     */
    private final class ScratchPointerWriter implements Runnable
    {

        private final MethodVisitor methodVisitor;

        private final int scratchPointerVarIndex;

        private final Integer[] previousVars;

        private ScratchPointerWriter( final MethodVisitor methodVisitor,
                                      final int scratchPointerVarIndex,
                                      final Integer[] previousVars )
        {
            this.methodVisitor = methodVisitor;
            this.scratchPointerVarIndex = scratchPointerVarIndex;
            this.previousVars = previousVars;
        }

        @Override
        public void run()
        {
            if ( NativeTypes.POINTER_SIZE == 4 )
            {
                this.methodVisitor.visitVarInsn( ILOAD, this.scratchPointerVarIndex );
            }
            else
            {
                this.methodVisitor.visitVarInsn( LLOAD, this.scratchPointerVarIndex );
            }

            if ( this.previousVars.length > 0 )
            {
                writeGetOffset( this.methodVisitor, this.previousVars );

                if ( NativeTypes.POINTER_SIZE == 4 )
                {
                    this.methodVisitor.visitInsn( IADD );
                }
                else
                {
                    this.methodVisitor.visitInsn( I2L );
                    this.methodVisitor.visitInsn( LADD );
                }
            }
        }
    }

}
